package cs6238.common;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.Socket;
import java.nio.ByteBuffer;

import cs6238.common.message.MessageFactory;
import cs6238.common.message.command.Command;
import cs6238.common.message.response.Response;



/**
 * This class is used for both the client and the server, it provides a
 * very thin layer over the raw socket, it provides sending/receiving of
 * datagrams and encryption.
 */
public abstract class Communication {
	
	//Constants
	public static final byte[] MESSAGE_HEADER = new byte[]{0xC, 0xA, 0xF, 0xE, 0xB, 0xA, 0xB, 0xE};
	public static final byte[] MESSAGE_FOOTER = new byte[]{0xD, 0xE, 0xA, 0xD, 0xB, 0xE, 0xE, 0xF};
	
	private InputStream _input;
	private OutputStream _output;
	
	private Socket _socket;
	
	
	
	public Communication(Socket socket) throws IOException{
		_input = socket.getInputStream();
		_output = socket.getOutputStream();
	}
	
	protected Communication(InputStream input, OutputStream output) throws IOException{
		_input = input;
		_output = output;
	}
	
	private void sendMessage(byte[] message) throws IOException{
		sendMessage(_output, message);
	}
	
	public static void sendMessage(OutputStream output, byte[] message) throws IOException{
		ByteBuffer bb = ByteBuffer.allocate(4);
		bb.putInt(message.length);
		output.write(MESSAGE_HEADER);
		output.write(bb.array());
		output.write(message);
		output.write(MESSAGE_FOOTER);
	}
	
	public void sendCommand(Command command) throws IOException{
		sendMessage(command.serialize());
	}
	
	public Command receiveCommand() throws IOException{
		return MessageFactory.createCommand(receiveMessage());
	}
	
	public void sendResponse(Response response) throws IOException{
		sendMessage(response.serialize());
	}
	
	public Response receiveResponse() throws IOException{
		return MessageFactory.createResponse(receiveMessage());
	}
	
	public static byte[] receiveMessage(InputStream input) throws IOException{
		byte[] messageHeader = new byte[8];
		input.read(messageHeader);
		verifyMessage(MESSAGE_HEADER, messageHeader);
		
		byte[] messageLength = new byte[4];
		input.read(messageLength);
		
		int length = ByteBuffer.wrap(messageLength).getInt();
		byte[] message = new byte[length];
		
		input.read(message);
		
		byte[] messageFooter = new byte[8];
		input.read(messageFooter);
		verifyMessage(MESSAGE_FOOTER, messageFooter);
		
		return message;
	}
	
	private byte[] receiveMessage() throws IOException{
		return receiveMessage(_input);
	}
	
	private static void verifyMessage(byte[] expected, byte[] actual) throws IOException {
		for(int i=0; i<expected.length; i++){
			if(expected[i] != actual[i]){
				throw new IOException("Header/Footer validation error index(" + i + "): " + expected[i] + " " + actual[i]);
			}
		}
	}

	public void receiveFileAndSaveToDisk(String filename) throws IOException {
		byte[] decryptedFile = receiveMessage();
		
		FileOutputStream fos = new FileOutputStream(filename);
		fos.write(decryptedFile);
		fos.close();
	}
	
	public byte[] receiveFile() throws IOException{
		return receiveMessage();
	}
	
	public void sendFile(String filename) throws IOException{
		long fileLength = new File(filename).length();
		InputStream is = new FileInputStream(filename);
		byte[] entireFile = new byte[(int)fileLength];
		is.read(entireFile);
		sendMessage(entireFile);
	}
	
	public void sendFileAsBytes(byte[] decryptedFile) throws IOException {
		sendMessage(decryptedFile);
	}
	
	public void endConnection() {
		try {
			if(_socket != null && _socket.isConnected()){
				_socket.close();
			}
		} catch (IOException e) {
			System.out.println("Error while closing socket");
		}
	}
}
